Die Boost C++ Bibliotheken
Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.
Unter Interprozesskommunikation versteht man Mechanismen, die den Datenaustausch zwischen Programmen ermöglichen, die auf dem gleichen Computer laufen. Es geht also explizit nicht um einen Datenaustausch über Netzwerke. Wenn Sie Daten zwischen Programmen austauschen möchten, die auf unterschiedlichen Computern laufen und über ein Netzwerk verbunden sind, schlagen Sie Kapitel 7, Asynchrone Ein- und Ausgabe auf, das sich um die Bibliothek Boost.Asio dreht.
In diesem Kapitel wird Ihnen die Bibliothek Boost.Interprocess vorgestellt. Diese Bibliothek bietet zahlreiche Klassen an, die betriebssystemspezifische Schnittstellen zur Interprozesskommunikation abstrahieren. Denn obwohl Konzepte zur Interprozesskommunikation auf verschiedenen Betriebssystemen sehr ähnlich sind, können die Schnittstellen sehr unterschiedlich aussehen. Boost.Interprocess ermöglicht nun den plattformunabhängigen Zugriff auf Funktionen zur Interprozesskommunikation in C++.
Während Sie zum Datenaustausch zwischen Programmen auch Boost.Asio verwenden können - also auch dann, wenn die Programme auf dem gleichen Computer laufen - ist im Allgemeinen die Performance mit Boost.Interprocess besser. Denn Boost.Interprocess greift auf genau die Betriebssystemschnittstellen zu, die für den Datenaustausch zwischen Programmen auf dem gleichen Computer optimiert sind. Boost.Interprocess sollte daher die erste Wahl sein, wenn kein Datenaustausch zwischen Programmen über Netzwerke geplant ist.
Shared Memory ist üblicherweise die schnellste Form der Interprozesskommunikation. Dabei wird ein Speicherbereich gleichzeitig mehreren Programmen zur Verfügung gestellt. So kann ein Programm Daten in diesen Speicherbereich ablegen, und ein anderes Programm kann die abgelegten Daten aus dem Speicherbereich lesen.
Boost.Interprocess bietet eine Klasse boost::interprocess::shared_memory_object an, die einen derartigen Speicherbereich repräsentiert. Um diese Klasse nutzen zu können, muss die Headerdatei boost/interprocess/shared_memory_object.hpp eingebunden werden.
#include <boost/interprocess/shared_memory_object.hpp>
#include <iostream>
int main()
{
boost::interprocess::shared_memory_object shdmem(boost::interprocess::open_or_create, "Highscore", boost::interprocess::read_write);
shdmem.truncate(1024);
std::cout << shdmem.get_name() << std::endl;
boost::interprocess::offset_t size;
if (shdmem.get_size(size))
std::cout << size << std::endl;
}
Der Konstruktor von boost::interprocess::shared_memory_object erwartet drei Parameter. Der erste Parameter gibt an, ob der shared memory erstellt oder nur geöffnet werden soll. Dem obigen Programm ist es egal: boost::interprocess::open_or_create bedeutet, dass der shared memory geöffnet wird, wenn er bereits existiert, anderfalls neu erstellt wird.
Das Öffnen setzt voraus, dass auf einen shared memory zugegriffen werden kann, der bereits erstellt wurde. Um Speicherbereiche identifizieren zu können, werden ihnen Namen gegeben. Der zweite Parameter, der dem Konstruktor von boost::interprocess::shared_memory_object übergeben wird, gibt den Namen an.
Der dritte und letzte Parameter legt fest, wie ein Programm auf den shared memory zugreifen kann. Obiges Programm darf den shared memory sowohl lesen als auch beschreiben, da boost::interprocess::read_write angegeben ist.
Nachdem ein Objekt vom Typ boost::interprocess::shared_memory_object erstellt wurde, gibt es im Betriebssystem einen entsprechenden shared memory. Der shared memory ist jedoch von Beginn an 0 Byte groß. Um den shared memory nutzen zu können, muss truncate() aufgerufen werden. Dieser Methode wird die Größe in Bytes übergeben, auf die der shared memory anwachsen soll. Für das obige Programm bedeutet das, dass der shared memory Platz für 1024 Bytes bietet.
Beachten Sie, dass Sie truncate() nur dann aufrufen dürfen, wenn Sie den shared memory mit boost::interprocess::read_write geöffnet haben. Andernfalls wird eine Ausnahme vom Typ boost::interprocess::interprocess_exception geworfen.
Sie können truncate() auch mehrfach aufrufen, um die Größe des shared memory anzupassen.
Wenn Sie so wie im obigen Programm einen shared memory erstellt haben, können Sie mit Methoden wie get_name() und get_size() den Namen und die Größe abfragen.
Da Sie den shared memory jedoch erstellt haben, um über diesen Speicherbereich Daten mit anderen Programmen auszutauschen, müssen Sie in irgendeiner Weise auf die 1024 Bytes zugreifen. Dazu müssen Sie den shared memory in den Speicherbereich eines Prozesses abbilden. Dies erfolgt mit Hilfe der Klasse boost::interprocess::mapped_region.
Wenn Sie sich wundern, warum zwei Klassen eingesetzt werden, um auf einen shared memory zuzugreifen: Die Klasse boost::interprocess::mapped_region kann auch andere Objekte in den Speicherbereich eines Prozesses abbilden. So bietet Boost.Interprocess eine Klasse boost::interprocess::file_mapping an, die quasi einen shared memory für eine Datei darstellt. Ein Objekt vom Typ boost::interprocess::file_mapping entspricht also einer Datei. So werden zum Beispiel Daten, die in ein derartiges Objekt hineingeschrieben werden, automatisch in der dem Objekt zugeordneten Datei gespeichert. Weil mit boost::interprocess::file_mapping eine Datei nicht komplett geladen wird, sondern erst mit boost::interprocess::mapped_region ein beliebiger Teil der Datei in den Speicherbereich abgebildet wird, ist es zum Beispiel möglich, mehrere Gigabyte große Dateien zu verarbeiten, die auf heutigen 32-bit-Systemen nicht vollständig in den Speicher geladen werden könnten.
#include <boost/interprocess/shared_memory_object.hpp>
#include <boost/interprocess/mapped_region.hpp>
#include <iostream>
int main()
{
boost::interprocess::shared_memory_object shdmem(boost::interprocess::open_or_create, "Highscore", boost::interprocess::read_write);
shdmem.truncate(1024);
boost::interprocess::mapped_region region(shdmem, boost::interprocess::read_write);
std::cout << std::hex << "0x" << region.get_address() << std::endl;
std::cout << std::dec << region.get_size() << std::endl;
boost::interprocess::mapped_region region2(shdmem, boost::interprocess::read_only);
std::cout << std::hex << "0x" << region2.get_address() << std::endl;
std::cout << std::dec << region2.get_size() << std::endl;
}
Um die Klasse boost::interprocess::mapped_region verwenden zu können, müssen Sie die Headerdatei boost/interprocess/mapped_region.hpp einbinden. Dem Konstruktor dieser Klasse müssen Sie als ersten Parameter ein Objekt vom Typ boost::interprocess::shared_memory_object übergeben. Der zweite Parameter legt wiederum fest, ob nur lesend oder auch schreibend auf den Speicherbereich zugegriffen werden kann.
Im obigen Programm werden zwei Objekte vom Typ boost::interprocess::mapped_region erstellt: Der shared memory namens Highscore wird zweimal in den Speicherbereich des Prozesses abgebildet. Das Programm gibt über den Aufruf der Methoden get_address() und get_size() die Adresse und die Größe des abgebildeten Speicherbereichs aus. Während get_size() in beiden Fällen den gleichen Wert zurückgibt, nämlich 1024, ist der Rückgabewert der beiden Aufrufe von get_address() verschieden.
Im folgenden Programm wird nun auf die abgebildeten Speicherbereiche zugegriffen, um zum Test eine Zahl zu speichern und zu lesen.
#include <boost/interprocess/shared_memory_object.hpp>
#include <boost/interprocess/mapped_region.hpp>
#include <iostream>
int main()
{
boost::interprocess::shared_memory_object shdmem(boost::interprocess::open_or_create, "Highscore", boost::interprocess::read_write);
shdmem.truncate(1024);
boost::interprocess::mapped_region region(shdmem, boost::interprocess::read_write);
int *i1 = static_cast<int*>(region.get_address());
*i1 = 99;
boost::interprocess::mapped_region region2(shdmem, boost::interprocess::read_only);
int *i2 = static_cast<int*>(region2.get_address());
std::cout << *i2 << std::endl;
}
Die Zahl 99 wird über region an den Anfang des 1024 Byte großen shared memory geschrieben. Dann wird an den Anfang des Speicherbereichs region2 zugegriffen und eine Zahl auf die Standardausgabe ausgegeben. Obwohl region und region2 zwei unterschiedliche Speicherbereiche im Prozess darstellen - deswegen war der Rückgabewert von get_address() im vorherigen Programm verschieden - gibt das Programm 99 aus. Denn sowohl region als auch region2 greifen auf den gleichen shared memory zu.
Normalerweise werden pro shared memory nicht mehrere Objekte vom Typ boost::interprocess::mapped_region in einem Programm verwendet. Es macht schließlich nicht allzuviel Sinn, über zwei Speicherbereiche auf den gleichen shared memory zuzugreifen. Das obige Programm dient lediglich zum besseren Verständnis der Zusammenhänge.
Wenn Sie einen shared memory löschen möchten, greifen Sie auf die statische Methode remove() zu, die von der Klasse boost::interprocess::shared_memory_object zur Verfügung gestellt wird. Sie müssen ihr lediglich den Namen des shared memory übergeben, der gelöscht werden soll.
Boost.Interprocess unterstützt auch in gewisser Weise das aus dem Kapitel zu smart pointern bekannte RAII-Konzept. So können Sie die Klasse boost::interprocess::remove_shared_memory_on_destroy verwenden, indem Sie dem Konstruktor den Namen eines existierenden shared memory übergeben. Wird das Objekt vom Typ dieser Klasse gelöscht, wird der shared memory automatisch im Destruktor freigegeben.
Beachten Sie, dass der Konstruktor von boost::interprocess::remove_shared_memory_on_destroy keinen shared memory erstellt oder öffnet. Die Klasse ist daher kein typischer Vertreter des RAII-Konzepts.
#include <boost/interprocess/shared_memory_object.hpp>
#include <iostream>
int main()
{
bool removed = boost::interprocess::shared_memory_object::remove("Highscore");
std::cout << removed << std::endl;
}
Wenn Sie remove() nicht aufrufen, bleibt der shared memory bestehen, auch wenn Ihr Programm endet. Es hängt dann vom Betriebssystem ab, ob und wann der shared memory gelöscht wird. Während viele Unix-Betriebssysteme inklusive Linux einen shared memory automatisch löschen, wenn das System neugestartet wird, müssen Sie unter Windows und Mac OS X remove() aufrufen. Diese beiden Betriebssysteme speichern einen shared memory als Datei ab, so dass unter diesen beiden Betriebssystemen ein shared memory auch nach einem Neustart existiert.
Unter Windows gibt es eine besondere Art von shared memory, der automatisch gelöscht wird, wenn das letzte Programm beendet wurde, das den shared memory verwendet hat. Sie können diesen shared memory verwenden, indem Sie auf die Klasse boost::interprocess::windows_shared_memory zugreifen. Diese Klasse ist in der Headerdatei boost/interprocess/windows_shared_memory.hpp definiert.
#include <boost/interprocess/windows_shared_memory.hpp>
#include <boost/interprocess/mapped_region.hpp>
#include <iostream>
int main()
{
boost::interprocess::windows_shared_memory shdmem(boost::interprocess::open_or_create, "Highscore", boost::interprocess::read_write, 1024);
boost::interprocess::mapped_region region(shdmem, boost::interprocess::read_write);
int *i1 = static_cast<int*>(region.get_address());
*i1 = 99;
boost::interprocess::mapped_region region2(shdmem, boost::interprocess::read_only);
int *i2 = static_cast<int*>(region2.get_address());
std::cout << *i2 << std::endl;
}
Beachten Sie, dass die Klasse boost::interprocess::windows_shared_memory keine Methode truncate() anbietet. Stattdessen müssen Sie die Größe vom shared memory als vierten Parameter dem Konstruktor übergeben.
Auch wenn die Klasse boost::interprocess::windows_shared_memory nicht portabel ist und nur unter Windows verwendet werden kann: Sie ist vor allem dann von Nutzen, wenn Sie mit anderen Windows-Anwendungen Daten austauschen wollen, die diese besondere Art von shared memory verwenden.
Sie haben im vorherigen Abschnitt die Klasse boost::interprocess::shared_memory_object kennengelernt, mit der sich ein shared memory erstellen und verwalten lässt. In der Praxis arbeiten Sie eher selten mit dieser Klasse, weil Sie in diesem Fall selbst Bytes im shared memory schreiben und lesen müssen. Als C++-Entwickler sind Sie es gewohnt, Objekte vom Typ einer Klasse zu erstellen, ohne sich darum kümmern zu müssen, wie und wo die Bytes dieser Objekte im Speicher abgelegt sind.
Boost.Interprocess bietet nun ein Konzept an, das sich managed shared memory nennt. Es wird durch die Klasse boost::interprocess::managed_shared_memory zur Verfügung gestellt, die in der Headerdatei boost/interprocess/managed_shared_memory.hpp definiert ist. Mit Hilfe dieses managed shared memory wird es möglich, Objekte derart zu instantiieren, dass sich der vom Objekt benötigte Speicher in einem shared memory befindet - und das Objekt dadurch für andere Programme, die auf den gleichen shared memory zugreifen, automatisch verfügbar wird.
#include <boost/interprocess/managed_shared_memory.hpp>
#include <iostream>
int main()
{
boost::interprocess::shared_memory_object::remove("Highscore");
boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "Highscore", 1024);
int *i = managed_shm.construct<int>("Integer")(99);
std::cout << *i << std::endl;
std::pair<int*, std::size_t> p = managed_shm.find<int>("Integer");
if (p.first)
std::cout << *p.first << std::endl;
}
Im obigen Programm wird ein managed shared memory namens Highscore mit einer Größe von 1024 Bytes geöffnet. Wird kein shared memory mit diesem Namen gefunden, wird er automatisch erstellt.
Während Sie bei einem shared memory, wie Sie ihn im vorherigen Abschnitt kennengelernt haben, nun direkt auf einzelne Bytes zugreifen, um Daten zu speichern oder zu lesen, rufen Sie für einen managed shared memory Methoden wie construct() auf. Diese Methode erwartet als Template-Parameter einen Datentypen - im obigen Programm ist int angegeben. Als Funktionsparameter wird ein Name übergeben, mit dem sich das entsprechende Objekt, das im managed shared memory erstellt wird, finden lässt. So bekommt im obigen Programm die int-Variable den Namen Integer.
Da construct() ein Proxy-Objekt zurückgibt, können Sie über eine weitere Klammer Parameter an das Proxy-Objekt übergeben und es auf diese Weise initialisieren. Die Syntax ähnelt demnach einem Konstruktoraufruf. Somit ist sichergestellt, dass Sie mit construct() nicht nur neue Objekte in einem managed shared memory erstellen können, sondern sich diese auch wie gewünscht initialisieren lassen.
Um nun auf ein Objekt im managed shared memory zuzugreifen, verwenden Sie die Methode find(). Sie übergeben Ihr den Namen des Objekts, das Sie suchen, und erhalten als Ergebnis einen Zeiger zurück. Wird kein Objekt mit dem angegebenen Namen gefunden, ist der Rückgabewert 0.
Wenn Sie sich den Quellcode im obigen Programm genau anschauen, sehen Sie, dass find() den Zeiger in einem Objekt vom Typ std::pair zurückgibt. Der Zeiger wird also nicht direkt, sondern über die Eigenschaft first zurückgegeben. Was aber wird in second angegeben?
#include <boost/interprocess/managed_shared_memory.hpp>
#include <iostream>
int main()
{
boost::interprocess::shared_memory_object::remove("Highscore");
boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "Highscore", 1024);
int *i = managed_shm.construct<int>("Integer")[10](99);
std::cout << *i << std::endl;
std::pair<int*, std::size_t> p = managed_shm.find<int>("Integer");
if (p.first)
{
std::cout << *p.first << std::endl;
std::cout << p.second << std::endl;
}
}
Im obigen Programm wird im Vergleich zum vorherigen nicht eine int-Variable erzeugt, sondern ein Array, das Platz für zehn Werte vom Typ int bietet. Dies geschieht, weil hinter dem Aufruf von construct() in eckigen Klammern die Zahl 10 angegeben ist. Beim Zugriff auf second wird genau diese 10 auf die Standardausgabe ausgegeben. Es ist daher möglich zu erkennen, ob es sich bei einem Objekt, das mit find() gefunden wurde, tatsächlich um ein einzelnes Objekt handelt oder um ein Array. Bei einzelnen Objekten ist second auf 1 gesetzt. Für Arrays ist die entsprechende Größe des Arrays angegeben.
Beachten Sie, dass im obigen Programm alle zehn Stellen im Array mit der Zahl 99 initialisiert werden. Es ist also nicht möglich, die Stellen mit unterschiedlichen Werten zu initialisieren.
Beachten Sie außerdem, dass construct() fehlschlägt, wenn es bereits ein Objekt mit dem angegebenen Namen im managed shared memory gibt. In diesem Fall gibt construct() 0 zurück. Wollen Sie das Objekt für den Fall, dass es bereits existiert, wiederverwenden, rufen Sie find_or_construct() auf. Sollte das Objekt tatsächlich schon existieren, wird nicht 0, sondern ein Zeiger auf das existierende Objekt zurückgegeben. In diesem Fall findet keine Initialisierung statt.
Die Methode construct() kann auch in einem anderen Fall fehlschlagen. Betrachten Sie dazu das folgende Beispiel.
#include <boost/interprocess/managed_shared_memory.hpp>
#include <iostream>
int main()
{
try
{
boost::interprocess::shared_memory_object::remove("Highscore");
boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "Highscore", 1024);
int *i = managed_shm.construct<int>("Integer")[4096](99);
}
catch (boost::interprocess::bad_alloc &ex)
{
std::cerr << ex.what() << std::endl;
}
}
Im obigen Programm wird versucht, ein Array vom Typ int zu erstellen, das 4096 Stellen umfasst. Der managed shared memory ist jedoch lediglich 1024 Bytes groß. Der benötigte Speicher kann daher vom managed shared memory nicht zur Verfügung gestellt werden. Es wird daher eine Ausnahme vom Typ boost::interprocess::bad_alloc geworfen.
Nachdem Sie gesehen haben, wie Objekte in einem managed shared memory erstellt und gefunden werden können, lernen Sie die Methode destroy() kennen, mit der Objekte gelöscht werden können.
#include <boost/interprocess/managed_shared_memory.hpp>
#include <iostream>
int main()
{
boost::interprocess::shared_memory_object::remove("Highscore");
boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "Highscore", 1024);
int *i = managed_shm.find_or_construct<int>("Integer")(99);
std::cout << *i << std::endl;
managed_shm.destroy<int>("Integer");
std::pair<int*, std::size_t> p = managed_shm.find<int>("Integer");
std::cout << p.first << std::endl;
}
Sie übergeben der Methode destroy() den Namen des Objekts, das Sie löschen möchten. Diese Methode gibt ein Ergebnis vom Typ bool zurück, falls Sie wissen möchten, ob ein Objekt mit dem angegebenen Namen gefunden und gelöscht wurde. Ein Objekt wird immer gelöscht, wenn es gefunden werden konnte. Gibt die Methode also false zurück, bedeutet das lediglich, dass kein Objekt mit dem angegebenen Namen gefunden werden konnte.
Neben destroy() steht eine Methode destroy_ptr() zur Verfügung, der Sie einen Zeiger auf ein Objekt im managed shared memory übergeben können. Sie können destroy_ptr() auch für Arrays verwenden.
Nachdem Sie gesehen haben, wie einfach es mit managed shared memory ist, Objekte in einem mit anderen Programmen gemeinsam genutzten Speicherbereich abzulegen, möchten Sie sicherlich auch Container aus der C++ Standardbibliothek verwenden. Da Container selbständig dynamisch Speicher reservieren, muss ihnen irgendwie mitgeteilt werden, dass dieser Speicher in einem shared memory reserviert werden soll und nicht wie üblich mit new da, wo es das Betriebssystem für sinnvoll hält.
Leider kommt in der Praxis die zusätzliche Schwierigkeit hinzu, dass viele Implementationen der C++ Standardbibliothek nicht flexibel genug sind, um die von ihnen angebotenen Container wie std::string oder std::list gemeinsam mit Boost.Interprocess zu verwenden. Das trifft zum Beispiel auch auf die Standardbibliothek zu, die von Microsoft mit Visual Studio 2008 ausgeliefert wird.
Um dennoch Entwicklern die Möglichkeit zu bieten, die aus dem C++ Standard bekannten Container verwenden zu können, bietet Boost.Interprocess im Namensraum boost::interprocess flexiblere Implementationen der gleichen Klassen an. So steht zum Beispiel eine Klasse boost::interprocess::string zur Verfügung, die genauso funktioniert wie std::string. Der Vorteil ist, dass Objekte vom Typ boost::interprocess::string auf alle Fälle in einem managed shared memory abgelegt werden können, selbst wenn das mit Objekten vom Typ std::string wie bei vielen heutigen Implementationen der C++ Standardbibliothek nicht funktioniert.
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/allocators/allocator.hpp>
#include <boost/interprocess/containers/string.hpp>
#include <iostream>
int main()
{
boost::interprocess::shared_memory_object::remove("Highscore");
boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "Highscore", 1024);
typedef boost::interprocess::allocator<char, boost::interprocess::managed_shared_memory::segment_manager> CharAllocator;
typedef boost::interprocess::basic_string<char, std::char_traits<char>, CharAllocator> string;
string *s = managed_shm.find_or_construct<string>("String")("Hello!", managed_shm.get_segment_manager());
s->insert(5, ", world");
std::cout << *s << std::endl;
}
Um wie im obigen Beispiel einen String im managed shared memory erstellen zu können, der bei einem Aufruf von insert() den zusätzlich benötigten Speicher automatisch im gleichen managed shared memory reserviert, muss ein entsprechender Datentyp definiert werden. Denn der Datentyp, auf dem der String basiert, darf nicht den Standardallokator aus dem C++ Standard verwenden, sondern muss auf einen Allokator von Boost.Interprocess zugreifen.
Boost.Interprocess bietet eine Klasse boost::interprocess::allocator an, die in der Headerdatei boost/interprocess/allocators/allocator.hpp definiert ist. Mit dieser Klasse kann ein Allokator erstellt werden, der den sogenannten Segment-Manager des managed shared memory verwendet. Dieser Segment-Manager ist für die Verwaltung des Speichers in einem managed shared memory verantwortlich. Mit dem neu definierten Allokator kann dann ein entsprechender Datentyp für den String definiert werden, wobei hier nun wie oben erklärt auf boost::interprocess::basic_string und nicht auf std::basic_string zugegriffen wird. Der neu definierte Datentyp - im obigen Beispiel einfach string genannt - basiert demnach auf der Implementation von boost::interprocess::basic_string und greift über den Allokator auf einen Segment-Manager zu. Damit nun die Instanz von string, die durch den Aufruf von find_or_construct() erstellt wird, weiß, auf welchen Segment-Manager genau sie zuzugreifen hat, wird der Zeiger auf den Segment-Manager als zweiter Parameter an den Konstruktor übergeben.
Neben boost::interprocess::string bietet Boost.Interprocess für viele andere aus dem C++ Standard bekannte Container eigene Implementationen an. So stehen zum Beispiel Container wie boost::interprocess::vector und boost::interprocess::map zur Verfügung, die in den entsprechenden Headerdateien boost/interprocess/containers/vector.hpp und boost/interprocess/containers/map.hpp definiert sind.
Wenn Sie in verschiedenen Programmen auf den gleichen managed shared memory zugreifen und Objekte erstellen, suchen und zerstören, werden diese Operationen automatisch synchronisiert ausgeführt. Wenn zwei Programme also zur gleichen Zeit versuchen, zwei Objekte mit unterschiedlichem Namen in einem managed shared memory zu erstellen, wird der Zugriff auf den Speicher derart synchronisiert, dass die beiden Funktionen nacheinander ausgeführt werden. Möchten Sie mehrere Operationen gemeinsam ausführen, ohne dass diese eventuell von Operationen in einem gleichzeitig ausgeführten Programm unterbrochen werden, bietet sich die Methode atomic_func() an.
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/bind.hpp>
#include <iostream>
void construct_objects(boost::interprocess::managed_shared_memory &managed_shm)
{
managed_shm.construct<int>("Integer")(99);
managed_shm.construct<float>("Float")(3.14);
}
int main()
{
boost::interprocess::shared_memory_object::remove("Highscore");
boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "Highscore", 1024);
managed_shm.atomic_func(boost::bind(construct_objects, boost::ref(managed_shm)));
std::cout << *managed_shm.find<int>("Integer").first << std::endl;
std::cout << *managed_shm.find<float>("Float").first << std::endl;
}
Sie übergeben der Methode atomic_func() als einzigen Parameter eine Funktion, die keinen Rückgabewert besitzt und keinen Parameter erwartet. Diese Funktion wird derart aufgerufen, dass Sie in ihr exklusiven Zugriff auf den managed shared memory haben - jedenfalls, was die Methoden zum Erstellen, Suchen und Löschen von Objekten betrifft. Hat zum Beispiel ein anderes Programm bereits einen Zeiger auf ein Objekt im managed shared memory erhalten, kann es über diesen Zeiger auf das Objekt zugreifen und es verändern.
Sie können mit Boost.Interprocess auch den Zugriff auf Objekte synchronisieren. Da Boost.Interprocess nicht wissen kann, wer wann auf die von Ihnen verwendeten Objekte wie zugreifen darf, müssen Sie die Zugriffe selbst synchronisieren. Die Klassen, die Boost.Interprocess zur Synchronisation zur Verfügung stellt, lernen Sie im nächsten Abschnitt kennen.
Boost.Interprocess ermöglicht es mehreren Programmen, einen shared memory gemeinsam zu nutzen. Da Programme gleichzeitig laufen, konkurrieren sie um den Zugriff auf Daten im shared memory. Weil ein shared memory also per se eine gemeinsam genutzte Ressource ist, muss Boost.Interprocess in irgendeiner Form die Synchronisation von Zugriffen unterstützen.
Wenn Sie an Synchronisation denken, denken Sie wohlmöglich an die Bibliothek Boost.Thread. In der Tat haben Sie im Kapitel 6, Multithreading verschiedene Konzepte wie Mutexe und Bedingungsvariablen kennengelernt, mit denen Threads synchronisiert werden können. Die Klassen, die Boost.Thread jedoch zur Synchronisation zur Verfügung stellt, können tatsächlich nur zur Synchronisation von Threads im gleichen Programm verwendet werden. Sie können Klassen aus Boost.Thread nicht verwenden, um mehrere Programme zu synchronisieren. Da das Problem jedoch das gleiche ist - in beiden Fällen geht es um einen synchronisierten Zugriff auf gemeinsam genutzte Ressourcen - werden Ihnen in diesem Abschnitt die gleichen Konzepte wiederbegegnen, die Sie bereits von Boost.Thread kennen.
Während sich in Multithreaded-Anwendungen, die auf Boost.Thread zugreifen, Synchronisationsobjekte wie Mutexe und Bedingungsvariablen im gleichen Programm befinden und somit allen Threads zur Verfügung stehen, gibt es bei shared memory das Problem, das voneinander unabhängige Programme sich Synchronisationsobjekte teilen müssen. Wenn also ein Programm einen Mutex erstellt, müssen andere Programme auf diesen Mutex irgendwie zugreifen können.
Boost.Interprocesses bietet zwei Arten von Synchronisationsobjekten an: Anonyme Synchronisationsobjekte werden direkt im shared memory abgelegt, so dass sie dort automatisch allen Programmen zugänglich sind. Anderen Synchronisationsobjekten werden Namen gegeben, so dass alle Programme über diese Namen auf die gleichen Synchronisationsobjekte zugreifen können. Diese Synchronisationsobjekte werden demnach nicht im shared memory abgelegt, sondern vom Betriebssystem anderweitig verwaltet.
Im folgenden Beispiel sehen Sie, wie Sie einen Mutex mit Namen erstellen und verwenden. Dazu wird auf die Klasse boost::interprocess::named_mutex zugegriffen, die in der Headerdatei boost/interprocess/sync/named_mutex.hpp definiert ist.
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/sync/named_mutex.hpp>
#include <iostream>
int main()
{
boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "shm", 1024);
int *i = managed_shm.find_or_construct<int>("Integer")();
boost::interprocess::named_mutex named_mtx(boost::interprocess::open_or_create, "mtx");
named_mtx.lock();
++(*i);
std::cout << *i << std::endl;
named_mtx.unlock();
}
Dem Konstruktor der Klasse boost::interprocess::named_mutex müssen Sie neben einem Parameter, der angibt, ob der Mutex geöffnet oder erstellt werden soll, einen Namen übergeben. Jedes Programm, das den Namen kennt, kann nun den gleichen Mutex öffnen. Um dann den Zugriff auf Daten im shared memory zu synchronisieren, muss ein Programm lediglich den Mutex in Beschlag nehmen, indem es lock() aufruft. Da ein Mutex immer nur von einem Programm in Beschlag genommen werden kann, muss ein anderes Programm gegebenenfalls warten, bis das erste Programm den Mutex wieder mit unlock() freigegeben hat. Wenn also ein Programm einen Mutex in Beschlag genommen hat, bedeutet das, dass es exklusiven Zugriff auf eine Ressource hat. Im obigen Beispiel ist diese Ressource eine int-Variable, die inkrementiert und auf die Standardausgabe ausgegeben wird.
Wenn Sie obiges Programm mehrfach starten, wird Ihnen bei jedem Programmstart ein um 1 erhöhter Wert ausgegeben. Auch dann, wenn Sie das Programm mehrfach gleichzeitig starten, ist dank dem Mutex sichergestellt, dass der Zugriff aller gleichzeitig laufender Prozesse auf den gleichen shared memory und die gleiche int-Variable synchronisiert stattfindet.
Im folgenden Programm wird ein anonymer Mutex vom Typ boost::interprocess::interprocess_mutex verwendet, der im shared memory abgelegt werden muss, um allen Programmen zugänglich zu sein. Er ist in der Headerdatei boost/interprocess/sync/interprocess_mutex.hpp definiert.
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/sync/interprocess_mutex.hpp>
#include <iostream>
int main()
{
boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "shm", 1024);
int *i = managed_shm.find_or_construct<int>("Integer")();
boost::interprocess::interprocess_mutex *mtx = managed_shm.find_or_construct<boost::interprocess::interprocess_mutex>("mtx")();
mtx->lock();
++(*i);
std::cout << *i << std::endl;
mtx->unlock();
}
Das Programm funktioniert genauso wie das vorherige. Der einzige Unterschied ist, dass dieser Mutex im shared memory abgelegt ist. Dazu wird wie bekannt die Methode construct() oder find_or_construct() der Klasse boost::interprocess::managed_shared_memory verwendet.
Die Klassen boost::interprocess::named_mutex und boost::interprocess::interprocess_mutex bieten neben der Methode lock() zusätzlich try_lock() und timed_lock() an. Diese Methoden funktionieren genauso wie die gleichnamigen Methoden der Mutex-Klassen aus der Bibliothek Boost.Thread.
Für den Fall, dass Sie rekursive Mutexe einsetzen möchten, bietet Boost.Interprocess die beiden Klassen boost::interprocess::named_recursive_mutex und boost::interprocess::interprocess_recursive_mutex an.
Während Mutexe nützlich sind, um exklusiven Zugriff auf gemeinsam genutzte Ressourcen zu erhalten, helfen Bedingungsvariablen zu steuern, wer wann exklusiven Zugriff haben muss. Die von Boost.Interprocess zur Verfügung gestellten Bedingungsvariablen funktionieren grundsätzlich nicht anders als die Bedingungsvariablen in Boost.Thread. Die Klassen besitzen auch sehr ähnliche Schnittstellen, so dass Sie, wenn Sie die Bedingungsvariablen von Boost.Thread kennen, mit den Bedingungsvariablen von Boost.Interprocess keinerlei Schwierigkeiten haben werden.
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/sync/named_mutex.hpp>
#include <boost/interprocess/sync/named_condition.hpp>
#include <boost/interprocess/sync/scoped_lock.hpp>
#include <iostream>
int main()
{
boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "shm", 1024);
int *i = managed_shm.find_or_construct<int>("Integer")(0);
boost::interprocess::named_mutex named_mtx(boost::interprocess::open_or_create, "mtx");
boost::interprocess::named_condition named_cnd(boost::interprocess::open_or_create, "cnd");
boost::interprocess::scoped_lock<boost::interprocess::named_mutex> lock(named_mtx);
while (*i < 10)
{
if (*i % 2 == 0)
{
++(*i);
named_cnd.notify_all();
named_cnd.wait(lock);
}
else
{
std::cout << *i << std::endl;
++(*i);
named_cnd.notify_all();
named_cnd.wait(lock);
}
}
named_cnd.notify_all();
boost::interprocess::shared_memory_object::remove("shm");
boost::interprocess::named_mutex::remove("mtx");
boost::interprocess::named_condition::remove("cnd");
}
Im obigen Programm wird eine Bedingungsvariable vom Typ boost::interprocess::named_condition verwendet. Diese Klasse ist in der Headerdatei boost/interprocess/sync/named_condition.hpp definiert. Da es sich um eine Bedingungsvariable mit Namen handelt, muss sie nicht in einem shared memory abgelegt werden.
Das Programm verwendet eine while-Schleife, um eine im shared memory abgelegte Variable vom Typ int zu inkrementieren. Während die int-Variable in jedem Schleifendurchgang um 1 erhöht wird, wird sie lediglich in jedem zweiten Schleifendurchgang auf die Standardausgabe ausgegeben: Es werden lediglich ungerade Zahlen ausgegeben.
Jedesmal, wenn die int-Variable um 1 erhöht wurde, wird auf die Bedingungsvariable named_cnd zugegriffen und wait() aufgerufen. Dieser Methode wird ein sogenannter Lock übergeben - im obigen Programm ist dies die Variable lock. Der Lock hat die gleiche von Boost.Thread bekannte Bedeutung: Er basiert auf dem RAII-Konzept und nimmt einen Mutex im Konstruktor in Beschlag. Im Destruktor wiederum wird der Mutex freigegeben.
Der Lock wird vor Beginn der while-Schleife erstellt, womit der Mutex quasi während der Ausführung des gesamten Programms in Beschlag genommen ist. Wenn er jedoch als Parameter der Methode wait() übergeben wird, wird er automatisch freigegeben.
Bedingungsvariablen werden verwendet, um zu warten, bis jemand ein Signal schickt, dass die Warterei beendet werden kann. Diese Synchronisation erfolgt über die beiden Methoden wait() und notify_all(). Wenn also nun ein Programm wait() aufruft, gibt es den entsprechende Mutex frei und wartet darauf, dass jemand notify_all() für die gleiche Bedingungsvariable aufruft.
Wenn Sie das Programm starten, stellen Sie fest, dass erstmal nicht viel zu passieren scheint: In der while-Schleife wird die int-Variable zwar von 0 auf 1 erhöht. Anschließend wartet das Programm dann aber mit wait() auf ein Signal. Damit dieses Signal irgendwann erfolgt, starten Sie das gleiche Programm ein zweites Mal.
Die zweite Instanz des Programms wird versuchen, den gleichen Mutex in Beschlag zu nehmen, bevor die while-Schleife gestartet wird. Das funktioniert, weil die erste Instanz den Mutex durch den Aufruf von wait() freigegeben hat. Die zweite Instanz kann entsprechend die while-Schleife ausführen. Da die erste Instanz die int-Variable im shared memory von 0 auf 1 gesetzt hat, wird nun der else-Zweig der if-Kontrollstruktur ausgeführt. Hier wird der aktuelle Wert der int-Variablen auf die Standardausgabe ausgeben, bevor sie anschließend um 1 erhöht wird.
Auch die zweite Instanz ruft nun wait() auf. Bevor dies geschieht - und das ist wichtig, damit die beiden Instanzen kooperieren - ruft sie jedoch notify_all() auf. Damit wird die erste Instanz informiert, dass der Aufruf von wait() zurückkehren kann. Die erste Instanz weiß, dass sie den Mutex wieder in Beschlag nehmen kann, muss sich jedoch einen Augenblick gedulden, da der Mutex momentan von der zweiten Instanz in Beschlag genommen ist. Da die zweite Instanz nach notify_all() jedoch gleich als nächstes wait() aufruft und damit den Mutex automatisch freigibt, kann die erste Instanz übernehmen.
So wechseln sich beide Instanzen ab und inkrementieren abwechselnd die int-Variable im shared memory. Lediglich eine Instanz aber gibt die Werte auf die Standardausgabe aus. Wenn die int-Variable die Zahl 10 speichert, wird die while-Schleife beendet. Damit die andere Instanz nicht endlos in wait() auf ein Signal wartet, wird nach der while-Schleife noch einmal notify_all() aufgerufen. Abschließend werden der verwendete shared memory, der Mutex und die Bedingungsvariable gelöscht.
So wie es zwei Arten von Mutex gibt - einen anonymen, der im shared memory abgelegt werden muss, und einen, der über einen Namen identifiziert wird - gibt es auch zwei Arten von Bedingungsvariablen. Das obige Programm wird nun derart umgeschrieben, dass eine anonyme Bedingungsvariable verwendet wird.
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/sync/interprocess_mutex.hpp>
#include <boost/interprocess/sync/interprocess_condition.hpp>
#include <boost/interprocess/sync/scoped_lock.hpp>
#include <iostream>
int main()
{
try
{
boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "shm", 1024);
int *i = managed_shm.find_or_construct<int>("Integer")(0);
boost::interprocess::interprocess_mutex *mtx = managed_shm.find_or_construct<boost::interprocess::interprocess_mutex>("mtx")();
boost::interprocess::interprocess_condition *cnd = managed_shm.find_or_construct<boost::interprocess::interprocess_condition>("cnd")();
boost::interprocess::scoped_lock<boost::interprocess::interprocess_mutex> lock(*mtx);
while (*i < 10)
{
if (*i % 2 == 0)
{
++(*i);
cnd->notify_all();
cnd->wait(lock);
}
else
{
std::cout << *i << std::endl;
++(*i);
cnd->notify_all();
cnd->wait(lock);
}
}
cnd->notify_all();
}
catch (...)
{
}
boost::interprocess::shared_memory_object::remove("shm");
}
Das Programm funktioniert genauso wie das vorherige und muss ebenfalls zweimal gestartet werden, damit die int-Variable zehnmal inkrementiert wird. Sie sehen auch, dass die Unterschiede zwischen diesem und dem vorherigen Programm minimal sind. Ob Sie anonyme Synchronisationsobjekte oder Synchronisationsobjekte mit Namen verwenden, spielt grundsätzlich keine Rolle.
Neben Mutexen und Bedingungsvariablen, die Sie in diesem Abschnitt kennengelernt haben, unterstützt Boost.Interprocess auch sogenannte Semaphore und File Locks. Semaphore funktionieren ähnlich wie Bedingungsvariablen, nur dass sie nicht zwischen zwei Zuständen unterscheiden, sondern auf einem Zähler basieren. File Locks wiederum funktionieren wie Mutexe, wobei es sich hierbei nicht um ein Objekt im Speicher handelt, sondern um eine Datei im Dateisystem.
So wie Boost.Thread verschiedene Typen von Mutexen und Locks unterscheidet, stehen auch in Boost.Interprocess mehrere Mutexe und Locks zur Verfügung. So gibt es neben den in den obigen Beispielen verwendeten Klassen Mutexe, die nicht nur exklusiv in Beschlag genommen werden können. Dies ist nützlich, wenn mehrere Programme Daten gleichzeitig lesen wollen, da ein Mutex lediglich für Schreibvorgänge exklusiv in Beschlag genommen werden muss. Entsprechend stehen verschiedene Klassen für Locks zur Verfügung, mit denen das RAII-Konzept auf die verschiedenen Mutexe angewandt werden kann.
Beachten Sie, dass Sie unbedingt unterschiedliche Namen verwenden sollten, wenn Sie nicht anonyme Synchronisationsobjekte verwenden. Obwohl es sich bei Mutexen und Bedingungsvariablen um unterschiedliche Objekte handelt, die auf unterschiedlichen Klassen basieren, trifft das nicht unbedingt auf die von Boost.Interprocess abstrahierten Betriebssystemschnittstellen zu. So werden zum Beispiel unter Windows für Mutexe und Bedingungsvariablen die gleichen Betriebssystemfunktionen verwendet. Wenn Sie Mutexen und Bedingungsvariablen also den gleichen Namen geben, weil es sich Ihrer Meinung nach sowieso um unterschiedliche Objekte handelt, wird Ihr Programm unter Windows nicht wie erwartet funktionieren.
Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.
Erstellen Sie eine Client/Server-Anwendung, die über shared memory miteinander kommuniziert. Wenn der Client gestartet wird, soll ihm der Name einer Datei als Kommandozeilenparameter übergeben werden. Diese Datei soll über den shared memory an den Server gesendet werden. Der Server soll die Datei dann lokal in dem Verzeichnis speichern, in dem er gestartet wurde.
Copyright © 2008-2010 Boris Schäling